// Ridah, cg_sound.c - parsing and use of sound script files

#include "cg_local.h"

// we have to define these static lists, since we can't alloc memory within the cgame

#define FILE_HASH_SIZE			1024
static	soundScript_t*		hashTable[FILE_HASH_SIZE];

#define	MAX_SOUND_SCRIPTS		4096
static	soundScript_t		soundScripts[MAX_SOUND_SCRIPTS];
int	numSoundScripts=0;

#define	MAX_SOUND_SCRIPT_SOUNDS	8192
static	soundScriptSound_t	soundScriptSounds[MAX_SOUND_SCRIPT_SOUNDS];
int	numSoundScriptSounds=0;

/*
================
return a hash value for the filename
================
*/
static long generateHashValue( const char *fname ) {
	int		i;
	long	hash;
	char	letter;

	hash = 0;
	i = 0;
	while (fname[i] != '\0') {
		letter = tolower(fname[i]);
		if (letter =='.') break;				// don't include extension
		if (letter =='\\') letter = '/';		// damn path names
		hash+=(long)(letter)*(i+119);
		i++;
	}
	hash &= (FILE_HASH_SIZE-1);
	return hash;
}

/*
==============
CG_SoundScriptPrecache

  returns the index+1 of the script in the global list, for fast calling
==============
*/
int CG_SoundScriptPrecache( const char *name ) {
	soundScriptSound_t *scriptSound;
	long hash;
	char *s;
	soundScript_t	*sound;
//	byte buf[1024];
	int i;

	if( !name || !name[0] )
		return 0;

	hash = generateHashValue( name );

	s = (char *)name;
	for( sound = hashTable[hash]; sound; sound = sound->nextHash ) {
		if( !Q_stricmp( s, sound->name ) ) {
			// found a match, precache these sounds
			scriptSound = sound->soundList;
			if( !sound->streaming ) {
				for( ; scriptSound; scriptSound = scriptSound->next ) {
					for( i = 0; i < scriptSound->numsounds; i++ ) {
						scriptSound->sounds[i].sfxHandle = 0;
//						scriptSound->sounds[i].sfxHandle = trap_S_RegisterSound( scriptSound->sounds[i].filename, qfalse );	// FIXME: make compressed settable through the soundscript
					}
				}
			} else /*if (cg_buildScript.integer)*/ {	// Enabled this permanently so that streaming sounds get touched within file system on startup
				for( ; scriptSound; scriptSound = scriptSound->next ) {
					for( i = 0; i < scriptSound->numsounds; i++ ) {
						// just open the file so it gets copied to the build dir
/*						fileHandle_t f;
						trap_FS_FOpenFile( scriptSound->sounds[i].filename, &f, FS_READ );					
						trap_FS_Read( buf, sizeof( buf ), f ); // read a few bytes so the operating system does a better job of caching it for us
						trap_FS_FCloseFile( f );*/
					}
				}
			}
			return sound->index+1;
		}
	}

	return 0;
}

/*
==============
CG_SoundPickOldestRandomSound
==============
*/
int CG_SoundPickOldestRandomSound( soundScript_t *sound, vec3_t org, int entnum ) {
	int oldestTime = 0; // TTimo: init
	soundScriptSound_t *oldestSound;
	soundScriptSound_t *scriptSound;

	oldestSound = NULL;
	scriptSound = sound->soundList;
	while( scriptSound ) {
		if( !oldestSound || (scriptSound->lastPlayed < oldestTime) ) {
			oldestTime = scriptSound->lastPlayed;
			oldestSound = scriptSound;
		}
		scriptSound = scriptSound->next;
	}

	if( oldestSound ) {
		int pos = rand() % oldestSound->numsounds;

		// play this sound
		if( !sound->streaming ) {
			if( !oldestSound->sounds[pos].sfxHandle ) {
				oldestSound->sounds[pos].sfxHandle = trap_S_RegisterSound( oldestSound->sounds[pos].filename, qfalse );	// FIXME: make compressed settable through the soundscript
			}
			trap_S_StartSound( org, entnum, sound->channel, oldestSound->sounds[pos].sfxHandle );
			return trap_S_GetSoundLength( oldestSound->sounds[pos].sfxHandle );
		} else {
			return trap_S_StartStreamingSound( oldestSound->sounds[pos].filename, sound->looping ? oldestSound->sounds[pos].filename : NULL, entnum, sound->channel, sound->attenuation );
		}
		oldestSound->lastPlayed = cg.time;
	} else {
		CG_Error( "Unable to locate a valid sound for soundScript: %s\n", sound->name );
	}

	return 0;
}

void CG_AddBufferedSoundScript( soundScript_t* sound ) {
	if( cg.numbufferedSoundScripts >= MAX_BUFFERED_SOUNDSCRIPTS ) {
		return;
	}

	cg.bufferSoundScripts[cg.numbufferedSoundScripts++] = sound;

	if( cg.numbufferedSoundScripts == 1 ) {
		cg.bufferedSoundScriptEndTime = cg.time + CG_SoundPickOldestRandomSound( cg.bufferSoundScripts[0], NULL, -1 );
	}
}

void CG_UpdateBufferedSoundScripts( void ) {
	int i;

	if( !cg.numbufferedSoundScripts ) {
		return;
	}

	if( cg.time > cg.bufferedSoundScriptEndTime ) {
		for( i = 1; i < MAX_BUFFERED_SOUNDSCRIPTS; i++ ) {
			cg.bufferSoundScripts[i-1] = cg.bufferSoundScripts[i];
		}
		
		cg.numbufferedSoundScripts--;

		if( !cg.numbufferedSoundScripts ) {
			return;
		}

		cg.bufferedSoundScriptEndTime = cg.time + CG_SoundPickOldestRandomSound( cg.bufferSoundScripts[0], NULL, -1 );
	}
}

/*
==============
CG_SoundPlaySoundScript

  returns qtrue is a script is found
==============
*/
int CG_SoundPlaySoundScript( const char *name, vec3_t org, int entnum, qboolean buffer ) {
	long hash;
	char *s;
	soundScript_t	*sound;

	if (!name || !name[0])
		return qfalse;

	hash = generateHashValue( name );

	s = (char *)name;
	sound = hashTable[hash];
	while (sound) {
		if (!Q_stricmp( s, sound->name )) {
			if( buffer ) {
				CG_AddBufferedSoundScript( sound );
				return 1;

			} else {
				// found a match, pick the oldest sound
				return CG_SoundPickOldestRandomSound( sound, org, entnum );
			}
		}
		sound = sound->nextHash;
	}

	CG_Printf( S_COLOR_YELLOW "WARNING: CG_SoundPlaySoundScript: cannot find sound script '%s'\n", name );
	return 0;
}

/*
==============
CG_SoundPlayIndexedScript

  returns qtrue is a script is found
==============
*/
void CG_SoundPlayIndexedScript( int index, vec3_t org, int entnum ) {
	soundScript_t	*sound;

	if (!index)
		return;

	if (index > numSoundScripts)
		return;

	sound = &soundScripts[index-1];
	// pick the oldest sound
	CG_SoundPickOldestRandomSound( sound, org, entnum );
}

/*
===============
CG_SoundParseSounds
===============
*/
static void CG_SoundParseSounds ( char *filename, char *buffer ) {
	char *token, **text;
	int s;
	long	hash;
	soundScript_t		sound;			// the current sound being read
	soundScriptSound_t	*scriptSound = NULL;
	qboolean	inSound, wantSoundName;

	s = 0;
	inSound = qfalse;
	wantSoundName = qtrue;
	text = &buffer;

	while ( 1 ) {
		token = COM_ParseExt( text, qtrue );
		if( !*token ) {
			if( inSound ) {
				CG_Error( "no concluding '}' in sound %s, file %s\n", sound.name, filename );
			}
			return;
		}

		if ( !Q_stricmp( token, "{" ) ) {
			if (inSound) {
				CG_Error( "no concluding '}' in sound %s, file %s\n", sound.name, filename );
			}
			if (wantSoundName) {
				CG_Error( "'{' found but not expected, after %s, file %s\n", sound.name, filename );
			}
			inSound = qtrue;

			// grab a free scriptSound
			scriptSound = &soundScriptSounds[numSoundScriptSounds++];

			if( numSoundScripts == MAX_SOUND_SCRIPT_SOUNDS ) {
				CG_Error( "MAX_SOUND_SCRIPT_SOUNDS exceeded.\nReduce number of sound scripts.\n" );
			}

			scriptSound->lastPlayed = 0;
			scriptSound->next = sound.soundList;
			scriptSound->numsounds = 0;
			sound.soundList = scriptSound;

			continue;
		}

		if ( !Q_stricmp( token, "}" ) ) {
			if (!inSound) {
				CG_Error( "'}' unexpected after sound %s, file %s\n", sound.name, filename );
			}
		
			// end of a sound, copy it to the global list and stick it in the hashTable
			hash = generateHashValue( sound.name );
			sound.nextHash = hashTable[hash];
			soundScripts[numSoundScripts] = sound;
			hashTable[hash] = &soundScripts[numSoundScripts++];

			if (numSoundScripts == MAX_SOUND_SCRIPTS) {
				CG_Error( "MAX_SOUND_SCRIPTS exceeded.\nReduce number of sound scripts.\n" );
			}

			inSound = qfalse;
			wantSoundName = qtrue;

			CG_SoundScriptPrecache( sound.name );

			continue;
		}

		if ( !inSound ) {
			// this is the identifier for a new sound
			if (!wantSoundName) {
				CG_Error( "'%s' unexpected after sound %s, file %s\n", token, sound.name, filename );
			}
			memset( &sound, 0, sizeof(sound) );
			Q_strncpyz( sound.name, token, sizeof(sound.name) );
			wantSoundName = qfalse;
			sound.index = numSoundScripts;
			// setup the new sound defaults
			sound.channel = CHAN_AUTO;
			sound.attenuation = 1;	// default to fade away with distance (for streaming sounds)
			//
			continue;
		}
		
		// we are inside a sound script

		if ( !Q_stricmp( token, "channel" ) ) {
			// ignore this now, just look for the channel identifiers explicitly
			continue;
		}
		if ( !Q_stricmp( token, "local" ) ) {
			sound.channel = CHAN_LOCAL;
			continue;
		} else if ( !Q_stricmp( token, "announcer" ) ) {
			sound.channel = CHAN_ANNOUNCER;
			continue;
		} else if ( !Q_stricmp( token, "body" ) ) {
			sound.channel = CHAN_BODY;
			continue;
		} else if ( !Q_stricmp( token, "voice" ) ) {
			sound.channel = CHAN_VOICE;
			continue;
		} else if ( !Q_stricmp( token, "weapon" ) ) {
			sound.channel = CHAN_WEAPON;
			continue;
		} else if ( !Q_stricmp( token, "item" ) ) {
			sound.channel = CHAN_ITEM;
			continue;
		} else if ( !Q_stricmp( token, "auto" ) ) {
			sound.channel = CHAN_AUTO;
			continue;
		}
		if ( !Q_stricmp( token, "global" ) ) {
			sound.attenuation = 0;
			continue;
		}
		if ( !Q_stricmp( token, "streaming" ) ) {
			sound.streaming = qtrue;
			continue;
		}
		if ( !Q_stricmp( token, "looping" ) ) {
			sound.looping = qtrue;
			continue;
		}
		if ( !Q_stricmp( token, "sound" ) ) {

			if( scriptSound->numsounds >= MAX_SOUNDSCRIPT_SOUNDS ) {
				CG_Error( "Too many sounds for soundscript %s\n" );
			}

			token = COM_ParseExt( text, qtrue );


			Q_strncpyz( scriptSound->sounds[scriptSound->numsounds].filename, token, sizeof(scriptSound->sounds[0].filename) );
			scriptSound->numsounds++;

			continue;
		}
	}
}

/*
===============
CG_SoundLoadSoundFiles
===============
*/
extern char bigTextBuffer[100000];	// we got it anyway, might as well use it

#define MAX_SOUND_FILES		128
static void CG_SoundLoadSoundFiles( void ) {
	char soundFiles[MAX_SOUND_FILES][MAX_QPATH];
	char *text;
	char filename[MAX_QPATH];
	fileHandle_t	f;
	int numSounds;
	int i, len;
	char *token;

	// scan for sound files
	Com_sprintf( filename, MAX_QPATH, "sound/scripts/filelist.txt" );
	len = trap_FS_FOpenFile( filename, &f, FS_READ );
	if ( len <= 0 ) {
		CG_Printf( S_COLOR_RED "WARNING: no sound files found (filelist.txt not found in sound/scripts)\n" );
		return;
	}
	if ( len > sizeof(bigTextBuffer) ) {
		CG_Error( "%s is too big, make it smaller (max = %i bytes)\n", filename, sizeof(bigTextBuffer) );
	}
	// load the file into memory
	trap_FS_Read( bigTextBuffer, len, f );
	bigTextBuffer[len] = 0;
	trap_FS_FCloseFile( f );
	// parse the list
	text = bigTextBuffer;
	numSounds = 0;
	while (1) {
		token = COM_ParseExt( &text, qtrue );
		if (!token[0]) {
			break;
		}
		Com_sprintf( soundFiles[numSounds++], MAX_QPATH, token );
	}

	// add the map specific soundfile
	Com_sprintf( soundFiles[numSounds++], MAX_QPATH, "%s.sounds", cgs.rawmapname );

	if ( !numSounds ) {
		CG_Printf( S_COLOR_RED "WARNING: no sound files found\n" );
		return;
	}

	// load and parse sound files
	for ( i = 0; i < numSounds; i++ )
	{
		Com_sprintf( filename, sizeof( filename ), "sound/scripts/%s", soundFiles[i] );
		CG_Printf( "...loading '%s'\n", filename );
		len = trap_FS_FOpenFile( filename, &f, FS_READ );
		if( len <= 0 )
		{
			if( i != (numSounds - 1) )
				CG_Error( "Couldn't load %s", filename );
			continue;
		}
		if( len > sizeof(bigTextBuffer) )
			CG_Error( "%s is too big, make it smaller (max = %i bytes)\n", filename, sizeof(bigTextBuffer) );
		memset( bigTextBuffer, 0, sizeof(bigTextBuffer) );
		trap_FS_Read( bigTextBuffer, len, f );
		trap_FS_FCloseFile( f );
		CG_SoundParseSounds( filename, bigTextBuffer );
	}
}

/*
==============
CG_SoundInit
==============
*/
void CG_SoundInit( void ) {

	if( numSoundScripts ) {
		// keep all the information, just reset the vars
		int i, j;

		for( i = 0; i < numSoundScriptSounds; i++ ) {
			soundScriptSounds[i].lastPlayed = 0;

			for( j = 0; j < soundScriptSounds[i].numsounds; j++ ) {
				soundScriptSounds[i].sounds[j].sfxHandle = 0;
			}
		}
	} else {
		CG_Printf(	"\n.........................\n"
					"Initializing Sound Scripts\n" );
		CG_SoundLoadSoundFiles ();
		CG_Printf(	"done.\n" );
	}

}

//
// Script Speakers
//

// Editing

typedef struct editHandle_s {
	vec3_t	origin;
	vec3_t	oldOrigin;

	int		activeAxis;
} editHandle_t;

static qhandle_t	speakerShader = 0;
static qhandle_t	speakerShaderGrayScale = 0;
static bg_speaker_t	*editSpeaker = NULL;
static bg_speaker_t	undoSpeaker;
static int			undoSpeakerIndex;
static qboolean		editSpeakerActive = qfalse;
static editHandle_t editSpeakerHandle;
static int			numSpeakersInPvs;

static const char *s_lt_string[] = {
	"no",
	"on",
	"off"
};

static const char *s_bt_string[] = {
	"no",
	"global",
	"nopvs"
};

qboolean CG_SaveSpeakersToScript( void )
{
	int				i;
	bg_speaker_t	*speaker;
	fileHandle_t	fh;
	char			*s;

	if( trap_FS_FOpenFile( va( "sound/maps/%s.sps", cgs.rawmapname ), &fh, FS_WRITE ) < 0 ) {
		CG_Printf( S_COLOR_RED "ERROR: failed to save speakers to 'sound/maps/%s.sps'\n", cgs.rawmapname );
		return qfalse;
	}

	s = "speakerScript\n{";
	trap_FS_Write( s, strlen(s), fh );

	for( i = 0; i < BG_NumScriptSpeakers(); i++ ) {
		char filenameStr[96] = "";
		char originStr[96];
		char targetnameStr[56] = "";
		char loopedStr[32];
		char broadcastStr[32];
		char waitStr[32] = "";
		char randomStr[32] = "";
		char volumeStr[32] = "";
		char rangeStr[32] = "";	

		speaker = BG_GetScriptSpeaker( i );

		if( *speaker->filename ) {
			Com_sprintf( filenameStr, sizeof(filenameStr), "\t\tnoise \"%s\"\n", speaker->filename );
		}

		Com_sprintf( originStr, sizeof(originStr), "\t\torigin %.2f %.2f %.2f\n", speaker->origin[0], speaker->origin[1], speaker->origin[2] );

		if( *speaker->targetname ) {
			Com_sprintf( targetnameStr, sizeof(targetnameStr), "\t\ttargetname \"%s\"\n", speaker->targetname );
		}

		Com_sprintf( loopedStr, sizeof(loopedStr), "\t\tlooped \"%s\"\n", s_lt_string[speaker->loop] );

		Com_sprintf( broadcastStr, sizeof(broadcastStr), "\t\tbroadcast \"%s\"\n", s_bt_string[speaker->broadcast] );

		if( speaker->wait ) {
			Com_sprintf( waitStr, sizeof(waitStr), "\t\twait %i\n", speaker->wait );
		}

		if( speaker->random ) {
			Com_sprintf( randomStr, sizeof(randomStr), "\t\trandom %i\n", speaker->random );
		}

		if( speaker->volume ) {
			Com_sprintf( volumeStr, sizeof(volumeStr), "\t\tvolume %i\n", speaker->volume );
		}

		if( speaker->range ) {
			Com_sprintf( rangeStr, sizeof(rangeStr), "\t\trange %i\n", speaker->range );
		}

		s = va( "\n\tspeakerDef {\n%s%s%s%s%s%s%s%s%s\t}\n",
				filenameStr,
				originStr,
				targetnameStr,
				loopedStr,
				broadcastStr,
				waitStr,
				randomStr,
				volumeStr,
				rangeStr );

		trap_FS_Write( s, strlen(s), fh );
	}

	s = "}\n";
	trap_FS_Write( s, strlen(s), fh );

	trap_FS_FCloseFile( fh );

	CG_Printf( "Saved %i speakers to 'sound/maps/%s.sps'\n", BG_NumScriptSpeakers(), cgs.rawmapname );

	return qtrue;
}

void CG_AddLineToScene( vec3_t start, vec3_t end, vec4_t colour )
{
	refEntity_t		re;

	memset( &re, 0, sizeof( re ) );
	re.reType = RT_RAIL_CORE;
	re.customShader = cgs.media.railCoreShader;
	VectorCopy( start, re.origin );
	VectorCopy( end, re.oldorigin );
	re.shaderRGBA[0] = colour[0] * 0xff;
	re.shaderRGBA[1] = colour[1] * 0xff;
	re.shaderRGBA[2] = colour[2] * 0xff;
	re.shaderRGBA[3] = colour[3] * 0xff;

	trap_R_AddRefEntityToScene( &re );
}

void CG_SetViewanglesForSpeakerEditor( void )
{
	vec3_t vec;

	if( !editSpeakerActive ) {
		return;
	}

	VectorSubtract( editSpeakerHandle.origin, cg.refdef_current->vieworg, vec );
	vectoangles( vec, cg.refdefViewAngles );
}

static void CG_RenderScriptSpeakers( void )
{
	int				i, j, closest;
	float			dist, minDist;
	vec3_t			vec;
	refEntity_t		re;
	bg_speaker_t	*speaker;

	closest = -1;
	minDist = Square( 8.f );

	/*{
		float	r, u;
		vec3_t	axisOrg, dir;

		closest = -1;
		minDist = Square( 32.f );

		r = -(cg.refdef_current->fov_x / 90.f) * (float)(cgs.cursorX - 320) / 320;
		u = -(cg.refdef_current->fov_y / 90.f) * (float)(cgs.cursorY - 240) / 240;

		for( i = 0; i < 3; i++ ) {
			dir[i] = cg.refdef_current->viewaxis[0][i] * 1.f +
					 cg.refdef_current->viewaxis[1][i] * r +
					 cg.refdef_current->viewaxis[2][i] * u;
		}
		VectorNormalizeFast( dir );

		VectorMA( cg.refdef_current->vieworg, 512, dir, vec );
		//VectorCopy( cg.refdef_current->vieworg, axisOrg );
		//axisOrg[2]+=.1f;
		VectorMA( cg.refdef_current->vieworg, .1f, cg.refdef_current->viewaxis[0], axisOrg );
		CG_AddLineToScene( vec, axisOrg, colorOrange );

	}*/

	numSpeakersInPvs = 0;

	for( i = 0; i < BG_NumScriptSpeakers(); i++ ) {
		speaker = BG_GetScriptSpeaker( i );

		if( editSpeakerActive && editSpeaker == speaker ) {
			vec4_t colour;
			for( j = 0; j < 3; j++ ) {
				VectorClear( colour );
				colour[3] = 1.f;
				if( editSpeakerHandle.activeAxis >= 0 ) {
					if( editSpeakerHandle.activeAxis == j ) {
						colour[j] = 1.f;
					} else {
						colour[j] = .3f;
					}
				} else {
					colour[j] = 1.f;
				}
				VectorClear( vec );
				vec[j] = 1.f;
				VectorMA( editSpeakerHandle.origin, 32, vec, vec );
				CG_AddLineToScene( editSpeakerHandle.origin, vec, colour );

				memset( &re, 0, sizeof( re ) );
				re.reType = RT_SPRITE;
				VectorCopy( vec, re.origin );
				VectorCopy( vec, re.oldorigin );
				re.radius = 3;
				re.customShader = cgs.media.waterBubbleShader;
				re.shaderRGBA[0] = colour[0] * 0xff;
				re.shaderRGBA[1] = colour[1] * 0xff;
				re.shaderRGBA[2] = colour[2] * 0xff;
				re.shaderRGBA[3] = colour[3] * 0xff;
				trap_R_AddRefEntityToScene( &re );
			}

			if( trap_R_inPVS( cg.refdef_current->vieworg, speaker->origin ) ) {
                numSpeakersInPvs++;
			}
		} else if( !trap_R_inPVS( cg.refdef_current->vieworg, speaker->origin ) ) {
			continue;
		} else {
			 numSpeakersInPvs++;
		}

		memset( &re, 0, sizeof( re ) );
		re.reType = RT_SPRITE;
		VectorCopy( speaker->origin, re.origin );
		VectorCopy( speaker->origin, re.oldorigin );
		re.radius = 8;
		re.customShader = speakerShader;

		if( editSpeaker ) {
			re.customShader = speakerShaderGrayScale;

			if( editSpeaker == speaker ) {
				re.shaderRGBA[0] = 0xff;
				re.shaderRGBA[1] = 0xaa;
				re.shaderRGBA[2] = 0xaa;
				re.shaderRGBA[3] = 0xff;
			} else {
				re.shaderRGBA[0] = 0x3f;
				re.shaderRGBA[1] = 0x3f;
				re.shaderRGBA[2] = 0x3f;
				re.shaderRGBA[3] = 0xff;
			}
		} else {
			re.customShader = speakerShader;

			re.shaderRGBA[0] = 0xff;
			re.shaderRGBA[1] = 0xff;
			re.shaderRGBA[2] = 0xff;
			re.shaderRGBA[3] = 0xff;
		}
		trap_R_AddRefEntityToScene( &re );

		// see which one is closest to our cursor
		if( !editSpeakerActive ) {
			VectorSubtract( speaker->origin, cg.refdef_current->vieworg, vec );
			dist = DotProduct( vec, cg.refdef_current->viewaxis[0] );
			VectorMA( cg.refdef_current->vieworg, dist, cg.refdef_current->viewaxis[0], vec );
			VectorSubtract( speaker->origin, vec, vec );
			dist = VectorLengthSquared( vec );
			if( dist <= minDist ) {
				minDist = dist;
				closest = i;
			}
		}
	}

	if( !editSpeakerActive ) {
		if( closest >= 0 ) {
			editSpeaker = BG_GetScriptSpeaker( closest );
		} else {
			editSpeaker = NULL;
		}
	}
}

void CG_SpeakerInfo_Text( panel_button_t* button )
{
	char *s, *ptr, *strptr;
	float y, wMax, w, h;
	vec4_t colour;
	char originStr[96];
	char filenameStr[96] = "";
	char targetnameStr[56] = "";
	char loopedStr[32];
	char broadcastStr[32];
	char waitStr[32] = "";
	char randomStr[32] = "";
	char volumeStr[32] = "";
	char rangeStr[32] = "";	

	if( !button->font ) {
		return;
	}

	Com_sprintf( originStr, sizeof(originStr), "Speaker at %.2f %.2f %.2f\n", editSpeaker->origin[0], editSpeaker->origin[1], editSpeaker->origin[2] );
	wMax = CG_Text_Width_Ext( originStr, button->font->scalex, 0, button->font->font );
	h = 8.5f;

	if( *editSpeaker->filename ) {
		Com_sprintf( filenameStr, sizeof(filenameStr), "noise: %s\n", editSpeaker->filename );
		w = CG_Text_Width_Ext( filenameStr, button->font->scalex, 0, button->font->font );
		if( w > wMax ) {
			wMax = w;
		}
		h += 8.5f;
	}

	if( *editSpeaker->targetname ) {
		Com_sprintf( targetnameStr, sizeof(targetnameStr), "targetname: %s\n", editSpeaker->targetname );
		w = CG_Text_Width_Ext( targetnameStr, button->font->scalex, 0, button->font->font );
		if( w > wMax ) {
			wMax = w;
		}
		h += 8.5f;
	}

	Com_sprintf( loopedStr, sizeof(loopedStr), "looped: %s\n", s_lt_string[editSpeaker->loop] );
	w = CG_Text_Width_Ext( loopedStr, button->font->scalex, 0, button->font->font );
	if( w > wMax ) {
		wMax = w;
	}
	h += 8.5f;

	Com_sprintf( broadcastStr, sizeof(broadcastStr), "broadcast: %s\n", s_bt_string[editSpeaker->broadcast] );
	w = CG_Text_Width_Ext( broadcastStr, button->font->scalex, 0, button->font->font );
	if( w > wMax ) {
		wMax = w;
	}
	h += 8.5f;

	if( editSpeaker->wait ) {
		Com_sprintf( waitStr, sizeof(waitStr), "wait: %i\n", editSpeaker->wait );
		w = CG_Text_Width_Ext( waitStr, button->font->scalex, 0, button->font->font );
		if( w > wMax ) {
			wMax = w;
		}
		h += 8.5f;
	}

	if( editSpeaker->random ) {
		Com_sprintf( randomStr, sizeof(randomStr), "random: %i\n", editSpeaker->random );
		w = CG_Text_Width_Ext( randomStr, button->font->scalex, 0, button->font->font );
		if( w > wMax ) {
			wMax = w;
		}
		h += 8.5f;
	}

	if( editSpeaker->volume ) {
		Com_sprintf( volumeStr, sizeof(volumeStr), "volume: %i\n", editSpeaker->volume );
		w = CG_Text_Width_Ext( volumeStr, button->font->scalex, 0, button->font->font );
		if( w > wMax ) {
			wMax = w;
		}
		h += 8.5f;
	}

	if( editSpeaker->range ) {
		Com_sprintf( rangeStr, sizeof(rangeStr), "range: %i\n", editSpeaker->range );
		w = CG_Text_Width_Ext( rangeStr, button->font->scalex, 0, button->font->font );
		if( w > wMax ) {
			wMax = w;
		}
		h += 8.5f;
	}

	VectorCopy( colorMdBlue, colour );
	colour[3] = .5f;
	CG_FillRect( button->rect.x - 2, button->rect.y - 2, wMax + 4, h + 4, colour );
	VectorCopy( colorBlue, colour );	
	CG_DrawRect( button->rect.x - 2, button->rect.y - 2, wMax + 4, h + 4, 1.f, colour );

	s = va( "%s%s%s%s%s%s%s%s%s",
				originStr,
				filenameStr,
				targetnameStr,
				loopedStr,
				broadcastStr,
				waitStr,
				randomStr,
				volumeStr,
				rangeStr );

	y = button->rect.y + 8;

	for( strptr = ptr = s; *ptr; ptr++ ) {
		if( *ptr == '\n' ) {
			*ptr = '\0';
			CG_Text_Paint_Ext( button->rect.x, y, button->font->scalex, button->font->scaley, button->font->colour, strptr, 0, 0, button->font->style, button->font->font );
			y += 8;
			strptr = ptr + 1;
		}
	}
}

panel_button_text_t speakerEditorTxt = {
	0.2f, 0.2f,
	{ 1.0f, 1.0f, 1.0f, 0.5f },
	ITEM_TEXTSTYLE_SHADOWED, 0,
	&cgs.media.limboFont2,
};

panel_button_t speakerInfo = {
	NULL,
	NULL,
	{ 344, 184, 272, 72 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	CG_SpeakerInfo_Text,
	NULL,
};

static panel_button_t *speakerInfoButtons[] = {
	&speakerInfo,
	NULL
};


void CG_SpeakerEditor_RenderEdit( panel_button_t* button )
{
	vec4_t colour;

	if( button == BG_PanelButtons_GetFocusButton() ) {
		VectorCopy( colorYellow, colour );
		colour[3] = .3f;
	} else {
		VectorCopy( colorWhite, colour );
		colour[3] = .3f;
	}
	CG_FillRect( button->rect.x, button->rect.y, button->rect.w, button->rect.h, colour );

	button->rect.x += 2.f;
	button->rect.h -= 3.f;
	BG_PanelButton_RenderEdit( button );
	button->rect.x -= 2.f;
 	button->rect.h += 3.f;
}

void CG_SpeakerEditor_RenderButton( panel_button_t* button )
{
	vec4_t colour;
	float oldX;

	if( button == BG_PanelButtons_GetFocusButton() ) {
		VectorCopy( colorMdBlue, colour );
		colour[3] = .5f;
	} else if( BG_PanelButtons_GetFocusButton() == NULL && BG_CursorInRect( &button->rect ) ) {
		VectorCopy( colorWhite, colour );
		colour[3] = .5f;
	} else {
		VectorCopy( colorWhite, colour );
		colour[3] = .3f;
	}
	CG_FillRect( button->rect.x, button->rect.y, button->rect.w, button->rect.h, colour );

	VectorCopy( colorBlue, colour );
	CG_DrawRect( button->rect.x, button->rect.y, button->rect.w, button->rect.h, 1.f, colour );

	oldX = button->rect.x;
	button->rect.x = button->rect.x + ( button->rect.w - CG_Text_Width_Ext( button->text, button->font->scalex, 0, button->font->font ) ) / 2.f;
	button->rect.y += 9.f;
	BG_PanelButtonsRender_Text( button );
	button->rect.x = oldX;
	button->rect.y -= 9.f;
}

char *CG_GetStrFromStrArray( const char *in, const int index )
{
	char *ptr, *s;
	int i;

	s = ptr = (char *)in;
	i = 0;
	while( 1 ) {
		if( i == index ) {
			return s;
		}

		while( *ptr ) {
			ptr++;
		}
		ptr++;

		s = ptr;
		i++;
	}
}

void CG_SpeakerEditor_RenderDropdown( panel_button_t* button )
{
	vec4_t colour;
	float textboxW;
	rectDef_t rect;
	int i;
	char *s;

	memcpy( &rect, &button->rect, sizeof(rect) );

	textboxW = button->rect.w - button->rect.h;
	rect.x += textboxW;
	rect.w = rect.h;

	if( button == BG_PanelButtons_GetFocusButton() ) {
		VectorCopy( colorYellow, colour );
		colour[3] = .3f;
	} else {
		VectorCopy( colorWhite, colour );
		colour[3] = .3f;
	}
	CG_FillRect( button->rect.x, button->rect.y, textboxW, button->rect.h, colour );
	VectorCopy( colorBlue, colour );
	CG_DrawRect( button->rect.x, button->rect.y, textboxW, button->rect.h, 1.f, colour );

	if( button == BG_PanelButtons_GetFocusButton() ) {
		VectorCopy( colorYellow, colour );
		colour[3] = .3f;
	} else if( BG_PanelButtons_GetFocusButton() == NULL && BG_CursorInRect( &button->rect ) ) {
		VectorCopy( colorWhite, colour );
		colour[3] = .5f;
	} else {
		VectorCopy( colorWhite, colour );
		colour[3] = .3f;
	}

	CG_FillRect( rect.x, rect.y, rect.w, rect.h, colour );
	VectorCopy( colorBlue, colour );
	CG_DrawRect( rect.x, rect.y, rect.w, rect.h, 1.f, colour );

    VectorCopy( button->font->colour, colour );
	CG_Text_Paint_Ext( rect.x + ( rect.w - CG_Text_Width_Ext( "V", button->font->scalex, 0, button->font->font ) ) / 2.f,
					   button->rect.y + 9.f,
					   button->font->scalex,
					   button->font->scaley,
					   colour,
					   "V",
					   0,
					   0,
					   0,
					   button->font->font );

	s = CG_GetStrFromStrArray( button->text, button->data[1] );

	CG_Text_Paint_Ext( button->rect.x + ( textboxW - CG_Text_Width_Ext( s, button->font->scalex, 0, button->font->font ) ) / 2.f,
					   button->rect.y + 9.f,
					   button->font->scalex,
					   button->font->scaley,
					   button->font->colour,
					   s,
					   0,
					   0,
					   button->font->style,
					   button->font->font );

	if( button == BG_PanelButtons_GetFocusButton() ) {
		memcpy( &rect, &button->rect, sizeof(rect) );

		for( i = 0; i < button->data[0]; i++ ) {
			if( i == button->data[1] )
				continue;

            rect.y += 12.f;

			if( BG_CursorInRect( &rect ) ) {
				VectorScale( colorYellow, .3f, colour );
				colour[3] = 1.f;
			} else {
				VectorScale( colorWhite, .3f, colour );
				colour[3] = 1.f;
			}

			CG_FillRect( rect.x, rect.y, rect.w, rect.h, colour );

			s = CG_GetStrFromStrArray( button->text, i );

			CG_Text_Paint_Ext( rect.x + ( textboxW - CG_Text_Width_Ext( s, button->font->scalex, 0, button->font->font ) ) / 2.f,
							   rect.y + 9.f,
							   button->font->scalex,
							   button->font->scaley,
							   button->font->colour,
							   s,
							   0,
							   0,
							   button->font->style,
							   button->font->font );
		}

		VectorCopy( colorBlue, colour );
		colour[3] = .3f;
		CG_DrawRect( button->rect.x, button->rect.y + 12.f, button->rect.w, rect.y - button->rect.y, 1.f, colour );
	}
}

void CG_SpeakerEditor_Back( panel_button_t* button )
{
	vec4_t colour;

	VectorCopy( colorMdBlue, colour );
	colour[3] = .5f;
	CG_FillRect( button->rect.x - 2, button->rect.y - 2,  button->rect.w + 4, button->rect.h + 4, colour );
	VectorCopy( colorBlue, colour );	
	CG_DrawRect( button->rect.x - 2, button->rect.y - 2, button->rect.w + 4, button->rect.h + 4, 1.f, colour );
}

void CG_SpeakerEditor_LocInfo( panel_button_t* button )
{
	CG_Text_Paint_Ext( button->rect.x, button->rect.y, button->font->scalex, button->font->scaley, button->font->colour,
					   va( "Speaker at %.2f %.2f %.2f", editSpeaker->origin[0], editSpeaker->origin[1], editSpeaker->origin[2] ),
					   0, 0, button->font->style, button->font->font );
}

static char noiseMatchString[MAX_QPATH];
static int noiseMatchCount;
static int noiseMatchIndex;

qboolean CG_SpeakerEditor_NoiseEdit_KeyDown( panel_button_t* button, int key )
{
	if( button == BG_PanelButtons_GetFocusButton() ) {
		if( key == K_TAB ) {
			char dirname[MAX_QPATH];
			char filename[MAX_QPATH];
			char match[MAX_QPATH];
			int i, numfiles, filelen;
			char *fileptr;

            COM_StripFilename( (char *)button->text, dirname );
			Q_strncpyz( filename, COM_SkipPath((char *)button->text), sizeof(filename) );

			if( !Q_stricmp( button->text, dirname ) ) {
				return qtrue;
			}

			numfiles = trap_FS_GetFileList( dirname, "", bigTextBuffer, sizeof(bigTextBuffer) );
			// FIXME: autocomplete directories?

			fileptr = bigTextBuffer;
			
			if( !*noiseMatchString || Q_stricmpn( noiseMatchString, filename, strlen(noiseMatchString) ) ) {
				Q_strncpyz( noiseMatchString, filename, sizeof(noiseMatchString) );
				noiseMatchCount = 0;
				noiseMatchIndex = 0;

				for( i = 0; i < numfiles; i++, fileptr += filelen + 1 ) {
					filelen = strlen(fileptr);
					if ( Q_stricmpn( fileptr, filename, strlen( filename ) ) ) {
						continue;
					}

					noiseMatchCount++;
					if( noiseMatchCount == 1 ) {
						Q_strncpyz( match, fileptr, sizeof(match) );
						continue;
					}

					/*if( strlen(fileptr) < strlen(match) ) {
						Q_strncpyz( match, fileptr, sizeof(match) );
						noiseMatchIndex++;
						continue;
					}*/
				}
			} else {
				if( noiseMatchCount == 1 ) {
					return qtrue;
				} else {
					int findMatchIndex = 0;

					noiseMatchIndex++;
					if( noiseMatchIndex == noiseMatchCount )
						noiseMatchIndex = 0;

					for( i = 0; i < numfiles; i++, fileptr += filelen + 1 ) {
						filelen = strlen(fileptr);
						if ( Q_stricmpn( fileptr, noiseMatchString, strlen( noiseMatchString ) ) ) {
							continue;
						}

						if( findMatchIndex == noiseMatchIndex ) {
							Q_strncpyz( match, fileptr, sizeof(match) );
							break;
						}

						findMatchIndex++;
					}
				}
			}

			if( noiseMatchCount == 0 ) {
				noiseMatchString[0] = '\0';
				return qtrue;
			}

			Com_sprintf( (char *)button->text, button->data[0], "%s%s", dirname, match );

			return qtrue;
		} else {
			if( key & K_CHAR_FLAG ) {
				int localkey = key;
				localkey &= ~K_CHAR_FLAG;
				if( localkey == 'h' - 'a' + 1 || localkey >= 32 ) {
					noiseMatchString[0] = '\0';
				}				
			}
		}
	}

    return BG_PanelButton_EditClick( button, key );
}

void CG_SpeakerEditor_NoiseEditFinish( panel_button_t* button )
{
	Q_strncpyz( editSpeaker->filename, button->text, sizeof(editSpeaker->filename) );
	
	if( *editSpeaker->filename ) {
		editSpeaker->noise = trap_S_RegisterSound( editSpeaker->filename, qfalse );
	} else {
		editSpeaker->noise = 0;
	}
}

void CG_SpeakerEditor_TargetnameEditFinish( panel_button_t* button )
{
	Q_strncpyz( editSpeaker->targetname, button->text, sizeof(editSpeaker->targetname) );
}

qboolean CG_SpeakerEditor_Dropdown_KeyDown( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		BG_PanelButtons_SetFocusButton( button );
		return qtrue;
	}

	return qfalse;
}

qboolean CG_SpeakerEditor_Looped_KeyUp( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		if( button == BG_PanelButtons_GetFocusButton() ) {
			rectDef_t rect;
			int i;

			memcpy( &rect, &button->rect, sizeof(rect) );

			for( i = 0; i < 3; i++ ) {
				if( i == editSpeaker->loop )
					continue;

				rect.y += 12.f;

				if( BG_CursorInRect( &rect ) ) {
					button->data[1] = editSpeaker->loop = i;
					break;
				}
			}

			if( editSpeaker->loop == S_LT_LOOPED_ON ) {
				editSpeaker->activated = qtrue;
			} else {
				editSpeaker->activated = qfalse;
			}

			BG_PanelButtons_SetFocusButton( NULL );

			return qtrue;
		}
	}

	return qfalse;
}

qboolean CG_SpeakerEditor_Broadcast_KeyUp( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		if( button == BG_PanelButtons_GetFocusButton() ) {
			rectDef_t rect;
			int i;

			memcpy( &rect, &button->rect, sizeof(rect) );

			for( i = 0; i < 3; i++ ) {
				if( i == editSpeaker->broadcast )
					continue;

				rect.y += 12.f;

				if( BG_CursorInRect( &rect ) ) {
					button->data[1] = editSpeaker->broadcast = i;
					break;
				}
			}

			BG_PanelButtons_SetFocusButton( NULL );

			return qtrue;
		}
	}

	return qfalse;
}

void CG_SpeakerEditor_WaitEditFinish( panel_button_t* button )
{
	if( *button->text ) {
		editSpeaker->wait = atoi(button->text);
		if( editSpeaker->wait < 0 ) {
			editSpeaker->wait = 0;
			Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->range );
		}
	} else {
		editSpeaker->wait = 0;
		Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->wait );
	}
}

void CG_SpeakerEditor_RandomEditFinish( panel_button_t* button )
{
	if( *button->text ) {
		editSpeaker->random = atoi(button->text);
		if( editSpeaker->random < 0 ) {
			editSpeaker->random = 0;
			Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->random );
		}
	} else {
		editSpeaker->random = 0;
		Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->random );
	}
}

void CG_SpeakerEditor_VolumeEditFinish( panel_button_t* button )
{
	if( *button->text ) {
		editSpeaker->volume = atoi(button->text);
		if( editSpeaker->volume < 0 ) {
			editSpeaker->volume = 0;
			Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->volume );
		} else if( editSpeaker->volume > 65535 ) {
			editSpeaker->volume = 65535;
			Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->volume );
		}
	} else {
		editSpeaker->volume = 127;
		Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->volume );
	}
}

void CG_SpeakerEditor_RangeEditFinish( panel_button_t* button )
{
	if( *button->text ) {
		editSpeaker->range = atoi(button->text);
		if( editSpeaker->range < 0 ) {
			editSpeaker->range = 0;
			Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->range );
		}
	} else {
		editSpeaker->range = 1250;
		Com_sprintf( (char*)button->text, sizeof(button->text), "%i", editSpeaker->range );
	}
}

qboolean CG_SpeakerEditor_Ok_KeyDown( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		BG_PanelButtons_SetFocusButton( button );
		return qtrue;
	}

	return qfalse;
}

qboolean CG_SpeakerEditor_Ok_KeyUp( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		if( button == BG_PanelButtons_GetFocusButton() ) {
			BG_PanelButtons_SetFocusButton( NULL );

			if( BG_CursorInRect( &button->rect ) ) {
				CG_SaveSpeakersToScript();

				editSpeakerActive = qfalse;
				CG_EventHandling( -CGAME_EVENT_SPEAKEREDITOR, qtrue );
			}
			return qtrue;
		}
	}

	return qfalse;
}

qboolean CG_SpeakerEditor_Cancel_KeyDown( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		BG_PanelButtons_SetFocusButton( button );
		return qtrue;
	}

	return qfalse;
}

qboolean CG_SpeakerEditor_Cancel_KeyUp( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		if( button == BG_PanelButtons_GetFocusButton() ) {
			BG_PanelButtons_SetFocusButton( NULL );

			if( BG_CursorInRect( &button->rect ) ) {
				memcpy( editSpeaker, &undoSpeaker, sizeof(*editSpeaker) );
				undoSpeakerIndex = -2;
				editSpeaker = NULL;
				editSpeakerActive = qfalse;
				CG_EventHandling( -CGAME_EVENT_SPEAKEREDITOR, qtrue );
			}
			return qtrue;
		}
	}

	return qfalse;
}

qboolean CG_SpeakerEditor_Delete_KeyDown( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		BG_PanelButtons_SetFocusButton( button );
		return qtrue;
	}

	return qfalse;
}

qboolean CG_SpeakerEditor_Delete_KeyUp( panel_button_t* button, int key )
{
	if( key == K_MOUSE1 ) {
		if( button == BG_PanelButtons_GetFocusButton() ) {
			BG_PanelButtons_SetFocusButton( NULL );

			if( BG_CursorInRect( &button->rect ) ) {
				undoSpeakerIndex = -1;
				BG_SS_DeleteSpeaker( BG_GetIndexForSpeaker( editSpeaker ) );
				CG_SaveSpeakersToScript();
				editSpeaker = NULL;
				editSpeakerActive = qfalse;
				CG_EventHandling( -CGAME_EVENT_SPEAKEREDITOR, qtrue );
			}
			return qtrue;
		}
	}

	return qfalse;
}

panel_button_t speakerEditorBack = {
	NULL,
	NULL,
	{ 360, 330, 272, 142 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	NULL,					/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	CG_SpeakerEditor_Back,
	NULL,
};

panel_button_t speakerEditorLocInfo = {
	NULL,
	NULL,
	{ 361, 330 + 9, 272, 10 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	CG_SpeakerEditor_LocInfo,
	NULL,
};

panel_button_t speakerEditorNoiseLabel = {
	NULL,
	"Noise:",
	{ 361, 344 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

char noiseEditBuffer[MAX_QPATH];

panel_button_t speakerEditorNoiseEdit = {
	NULL,
	noiseEditBuffer,
	{ 430, 344, 200, 12 },
	{ MAX_QPATH, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	CG_SpeakerEditor_NoiseEdit_KeyDown,	/* keyDown	*/
	NULL,								/* keyUp	*/
	CG_SpeakerEditor_RenderEdit,
	CG_SpeakerEditor_NoiseEditFinish,
};

panel_button_t speakerEditorTargetnameLabel = {
	NULL,
	"Targetname:",
	{ 361, 358 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

char targetnameEditBuffer[32];

panel_button_t speakerEditorTargetnameEdit = {
	NULL,
	targetnameEditBuffer,
	{ 430, 358, 200, 12 },
	{ 32, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	BG_PanelButton_EditClick,			/* keyDown	*/
	NULL,								/* keyUp	*/
	CG_SpeakerEditor_RenderEdit,
	CG_SpeakerEditor_TargetnameEditFinish,
};

panel_button_t speakerEditorLoopedLabel = {
	NULL,
	"Looped:",
	{ 361, 372 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

panel_button_t speakerEditorLoopedDropdown = {
	NULL,
	"no\0on\0off",
	{ 430, 372, 60, 12 },
	{ 3, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	CG_SpeakerEditor_Dropdown_KeyDown,	/* keyDown	*/
	CG_SpeakerEditor_Looped_KeyUp,		/* keyUp	*/
	CG_SpeakerEditor_RenderDropdown,
	NULL,
};

panel_button_t speakerEditorBroadcastLabel = {
	NULL,
	"Broadcast:",
	{ 361, 386 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

panel_button_t speakerEditorBroadcastDropdown = {
	NULL,
	"no\0global\0nopvs",
	{ 430, 386, 60, 12 },
	{ 3, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	CG_SpeakerEditor_Dropdown_KeyDown,	/* keyDown	*/
	CG_SpeakerEditor_Broadcast_KeyUp,	/* keyUp	*/
	CG_SpeakerEditor_RenderDropdown,
	NULL,
};

panel_button_t speakerEditorWaitLabel = {
	NULL,
	"Wait:",
	{ 361, 400 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

char waitEditBuffer[12];

panel_button_t speakerEditorWaitEdit = {
	NULL,
	waitEditBuffer,
	{ 430, 400, 200, 12 },
	{ 12, 2, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	BG_PanelButton_EditClick,			/* keyDown	*/
	NULL,								/* keyUp	*/
	CG_SpeakerEditor_RenderEdit,
	CG_SpeakerEditor_WaitEditFinish,
};

panel_button_t speakerEditorRandomLabel = {
	NULL,
	"Random:",
	{ 361, 414 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,		/* font		*/
	NULL,					/* keyDown	*/
	NULL,					/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

char randomEditBuffer[12];

panel_button_t speakerEditorRandomEdit = {
	NULL,
	randomEditBuffer,
	{ 430, 414, 200, 12 },
	{ 12, 2, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	BG_PanelButton_EditClick,			/* keyDown	*/
	NULL,								/* keyUp	*/
	CG_SpeakerEditor_RenderEdit,
	CG_SpeakerEditor_RandomEditFinish,
};

panel_button_t speakerEditorVolumeLabel = {
	NULL,
	"Volume:",
	{ 361, 428 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	NULL,								/* keyDown	*/
	NULL,								/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

char volumeEditBuffer[12];

panel_button_t speakerEditorVolumeEdit = {
	NULL,
	volumeEditBuffer,
	{ 430, 428, 200, 12 },
	{ 12, 2, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	BG_PanelButton_EditClick,			/* keyDown	*/
	NULL,								/* keyUp	*/
	CG_SpeakerEditor_RenderEdit,
	CG_SpeakerEditor_VolumeEditFinish,
};

panel_button_t speakerEditorRangeLabel = {
	NULL,
	"Range:",
	{ 361, 442 + 9, 0, 0 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	NULL,								/* keyDown	*/
	NULL,								/* keyUp	*/
	BG_PanelButtonsRender_Text,
	NULL,
};

char rangeEditBuffer[12];

panel_button_t speakerEditorRangeEdit = {
	NULL,
	rangeEditBuffer,
	{ 430, 442, 200, 12 },
	{ 12, 2, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	BG_PanelButton_EditClick,			/* keyDown	*/
	NULL,								/* keyUp	*/
	CG_SpeakerEditor_RenderEdit,
	CG_SpeakerEditor_RangeEditFinish,
};

panel_button_t speakerEditorOkButton = {
	NULL,
	"Ok",
	{ 376, 458, 70, 12 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	CG_SpeakerEditor_Ok_KeyDown,		/* keyDown	*/
	CG_SpeakerEditor_Ok_KeyUp,			/* keyUp	*/
	CG_SpeakerEditor_RenderButton,
	NULL,
};

panel_button_t speakerEditorCancelButton = {
	NULL,
	"Cancel",
	{ 461, 458, 70, 12 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	CG_SpeakerEditor_Cancel_KeyDown,	/* keyDown	*/
	CG_SpeakerEditor_Cancel_KeyUp,		/* keyUp	*/
	CG_SpeakerEditor_RenderButton,
	NULL,
};

panel_button_t speakerEditorDeleteButton = {
	NULL,
	"Delete",
	{ 546, 458, 70, 12 },
	{ 0, 0, 0, 0, 0, 0, 0, 0 },
	&speakerEditorTxt,					/* font		*/
	CG_SpeakerEditor_Delete_KeyDown,	/* keyDown	*/
	CG_SpeakerEditor_Delete_KeyUp,		/* keyUp	*/
	CG_SpeakerEditor_RenderButton,
	NULL,
};

static panel_button_t *speakerEditorButtons[] = {
	&speakerEditorBack,
	&speakerEditorLocInfo,
	&speakerEditorNoiseLabel,
	&speakerEditorNoiseEdit,
	&speakerEditorTargetnameLabel,
	&speakerEditorTargetnameEdit,
	&speakerEditorLoopedLabel,
	&speakerEditorBroadcastLabel,
	&speakerEditorWaitLabel,
	&speakerEditorWaitEdit,
	&speakerEditorRandomLabel,
	&speakerEditorRandomEdit,
	&speakerEditorVolumeLabel,
	&speakerEditorVolumeEdit,
	&speakerEditorRangeLabel,
	&speakerEditorRangeEdit,
	&speakerEditorOkButton,
	&speakerEditorCancelButton,
	&speakerEditorDeleteButton,

	// Below here all components that should draw on top
	&speakerEditorBroadcastDropdown,
	&speakerEditorLoopedDropdown,
	NULL
};

void CG_SpeakerEditorDraw( void )
{
	if( !cg.editingSpeakers ) {
		return;
	}

	if( !editSpeakerActive ) {
		int bindingkey[2];
		char binding[2][32];
		vec4_t colour;
		float x, y, w, h;

		VectorCopy( colorWhite, colour );
		colour[3] = .8f;

		if( undoSpeakerIndex == -2 )
			y = 452;
		else
			y = 442;

		CG_Text_Paint_Ext( 8, y, .2f, .2f, colour,
						va( "Current amount of speakers in map: %i (inpvs: %i max in map: %i)",
														BG_NumScriptSpeakers(),
														numSpeakersInPvs,
														256 ),
						0, 0, ITEM_TEXTSTYLE_SHADOWED, &cgs.media.limboFont2 );

		trap_Key_KeysForBinding( "dumpspeaker", &bindingkey[0], &bindingkey[1] );
		trap_Key_KeynumToStringBuf( bindingkey[0], binding[0], sizeof(binding[0]) );
		trap_Key_KeynumToStringBuf( bindingkey[1], binding[1], sizeof(binding[1]) );
		Q_strupr( binding[0] );
		Q_strupr( binding[1] );
		CG_Text_Paint_Ext( 8, y + 10, .2f, .2f, colour,
						va( "Create new speaker: %s%s", bindingkey[0] != -1 ? binding[0] : "???",
														bindingkey[1] != -1 ? va( " or %s", binding[1] ) : "" ),
						0, 0, ITEM_TEXTSTYLE_SHADOWED, &cgs.media.limboFont2 );

		trap_Key_KeysForBinding( "modifyspeaker", &bindingkey[0], &bindingkey[1] );
		trap_Key_KeynumToStringBuf( bindingkey[0], binding[0], sizeof(binding[0]) );
		trap_Key_KeynumToStringBuf( bindingkey[1], binding[1], sizeof(binding[1]) );
		Q_strupr( binding[0] );
		Q_strupr( binding[1] );
		CG_Text_Paint_Ext( 8, y + 20, .2f, .2f, colour,
						va( "Modify target speaker: %s%s", bindingkey[0] != -1 ? binding[0] : "???",
															bindingkey[1] != -1  ? va( " or %s", binding[1] ) : "" ),
						0, 0, ITEM_TEXTSTYLE_SHADOWED, &cgs.media.limboFont2 );

		if( undoSpeakerIndex != -2 ) {
			trap_Key_KeysForBinding( "undospeaker", &bindingkey[0], &bindingkey[1] );
			trap_Key_KeynumToStringBuf( bindingkey[0], binding[0], sizeof(binding[0]) );
			trap_Key_KeynumToStringBuf( bindingkey[1], binding[1], sizeof(binding[1]) );
			Q_strupr( binding[0] );
			Q_strupr( binding[1] );
			CG_Text_Paint_Ext( 8, y + 30, .2f, .2f, colour,
							va( "Undo %s speaker: %s%s",	undoSpeakerIndex == -1 ? "remove" : "modify",
															bindingkey[0] != -1 ? binding[0] : "???",
															bindingkey[1] != -1  ? va( " or %s", binding[1] ) : "" ),
							0, 0, ITEM_TEXTSTYLE_SHADOWED, &cgs.media.limboFont2 );
		}

		// render crosshair

		x = cg_crosshairX.integer;
		y = cg_crosshairY.integer;
		w = h = cg_crosshairSize.value;

		CG_AdjustFrom640( &x, &y, &w, &h );

		trap_R_DrawStretchPic( x + 0.5 * (cg.refdef_current->width - w),
							   y + 0.5 * (cg.refdef_current->height - h),
							   w, h, 0, 0, 1, 1, cgs.media.crosshairShader[ cg_drawCrosshair.integer % NUM_CROSSHAIRS ] );

		if( cg.crosshairShaderAlt[ cg_drawCrosshair.integer % NUM_CROSSHAIRS ] ) {
			trap_R_DrawStretchPic( x + 0.5 * (cg.refdef_current->width - w),
								   y + 0.5 * (cg.refdef_current->height - h),
								   w, h, 0, 0, 1, 1, cg.crosshairShaderAlt[ cg_drawCrosshair.integer % NUM_CROSSHAIRS ] );
		}


		if( editSpeaker ) {
			// render interface
			BG_PanelButtonsRender( speakerInfoButtons );
		}

	} else {
		// render interface
		BG_PanelButtonsRender( speakerEditorButtons );

		// render cursor
		trap_R_SetColor( NULL );
		CG_DrawPic( cgDC.cursorx, cgDC.cursory, 32, 32, cgs.media.cursorIcon );
	}
}

void CG_SpeakerEditor_KeyHandling( int key, qboolean down )
{
	if( !BG_PanelButtonsKeyEvent( key, down, speakerEditorButtons ) ) {
		switch( key ) {
		case K_MOUSE1:	if( !down ) {
							editSpeakerHandle.activeAxis = -1;
							break;
						} else if( editSpeakerHandle.activeAxis == -1 ) {
							int		i, closest;
							float	dist, minDist, r, u;
							vec3_t	vec, axisOrg, dir;

							closest = -1;
							minDist = Square( 16.f );

							r = -(cg.refdef_current->fov_x / 90.f) * (float)(cgs.cursorX - 320) / 320;
							u = -(cg.refdef_current->fov_y / 90.f) * (float)(cgs.cursorY - 240) / 240;

							for( i = 0; i < 3; i++ ) {
								dir[i] = cg.refdef_current->viewaxis[0][i] * 1.f +
										 cg.refdef_current->viewaxis[1][i] * r +
										 cg.refdef_current->viewaxis[2][i] * u;
							}
							VectorNormalizeFast( dir );

							for( i = 0; i < 3; i++ ) {
								VectorClear( vec );
								vec[i] = 1.f;
								VectorMA( editSpeakerHandle.origin, 32, vec, axisOrg );

								// see which one is closest to our cursor
								VectorSubtract( axisOrg, cg.refdef_current->vieworg, vec );
								dist = DotProduct( vec, dir );
								VectorMA( cg.refdef_current->vieworg, dist, dir, vec );
								dist = DistanceSquared( axisOrg, vec );
								if( dist <= minDist ) {
									minDist = dist;
									closest = i;
								}
							}

							editSpeakerHandle.activeAxis = closest;

							if( editSpeakerHandle.activeAxis >= 0 ) {								
								VectorCopy( editSpeakerHandle.origin, editSpeakerHandle.oldOrigin );
							};
						}
						break;
		case K_ESCAPE:	BG_PanelButtons_SetFocusButton( NULL );
						CG_SaveSpeakersToScript();
						editSpeakerActive = qfalse;
						CG_EventHandling( -CGAME_EVENT_SPEAKEREDITOR, qtrue );
						break;
		}
	}
}

void CG_SpeakerEditorMouseMove_Handling( int x, int y )
{
	if( !cg.editingSpeakers ) {
		return;
	}

	if( editSpeakerActive ) {
		if( editSpeakerHandle.activeAxis >= 0 ) {
			if( editSpeakerHandle.activeAxis == 0 ) {
				// this one and the next one are quite nasty, so do it the hacky way
				if( cgs.cursorX - x < 320 ) {
					editSpeaker->origin[0] -= x;
				} else {
					editSpeaker->origin[0] += x;
				}
			} else if( editSpeakerHandle.activeAxis == 1 ) {
				if( cgs.cursorX - x < 320 ) {
					editSpeaker->origin[1] -= x;
				} else {
					editSpeaker->origin[1] += x;
				}
			} else if( editSpeakerHandle.activeAxis == 2 ) {
				// but this one is easy
				editSpeaker->origin[2] -= y;
			}

			cgs.cursorX -= x;
			cgs.cursorY -= y;

			VectorCopy( editSpeakerHandle.origin, editSpeakerHandle.oldOrigin );
			VectorCopy( editSpeaker->origin, editSpeakerHandle.origin );
		}
	}
}

void CG_ActivateEditSoundMode( void )
{	
	CG_Printf( "Activating Speaker Edit mode.\n" );
	cg.editingSpeakers = qtrue;
	
	editSpeaker = NULL;
	editSpeakerActive = qfalse;
	editSpeakerHandle.activeAxis = -1;
	undoSpeakerIndex = -2;

	if( !speakerShader ) {
		speakerShader = trap_R_RegisterShader( "gfx/misc/speaker" );
		speakerShaderGrayScale = trap_R_RegisterShader( "gfx/misc/speaker_gs" );

		BG_PanelButtonsSetup( speakerInfoButtons );
		BG_PanelButtonsSetup( speakerEditorButtons );
	}
}

void CG_DeActivateEditSoundMode( void )
{
	CG_Printf( "De-activating Speaker Edit mode.\n" );
	cg.editingSpeakers = qfalse;

	if( editSpeakerActive ) {
		CG_EventHandling( -CGAME_EVENT_SPEAKEREDITOR, qtrue );
	}

	editSpeaker = NULL;
	editSpeakerActive = qfalse;
	editSpeakerHandle.activeAxis = -1;
	undoSpeakerIndex = -2;
}

void CG_ModifyEditSpeaker( void )
{
	if( !editSpeaker || editSpeakerActive ) {
		return;
	}

	CG_EventHandling( CGAME_EVENT_SPEAKEREDITOR, qfalse );

	editSpeakerActive = qtrue;
	memcpy( &undoSpeaker, editSpeaker, sizeof(undoSpeaker) );
	undoSpeakerIndex = BG_GetIndexForSpeaker( editSpeaker );

	VectorCopy( editSpeaker->origin, editSpeakerHandle.origin );
	VectorCopy( editSpeaker->origin, editSpeakerHandle.oldOrigin );

	Q_strncpyz( noiseEditBuffer, editSpeaker->filename, sizeof(noiseEditBuffer) );
	Q_strncpyz( targetnameEditBuffer, editSpeaker->targetname, sizeof(targetnameEditBuffer) );
	speakerEditorLoopedDropdown.data[1] = editSpeaker->loop;
	speakerEditorBroadcastDropdown.data[1] = editSpeaker->broadcast;
	Com_sprintf( waitEditBuffer, sizeof(waitEditBuffer), "%i", editSpeaker->wait );
	Com_sprintf( randomEditBuffer, sizeof(randomEditBuffer), "%i", editSpeaker->random );
	Com_sprintf( volumeEditBuffer, sizeof(volumeEditBuffer), "%i", editSpeaker->volume );
	Com_sprintf( rangeEditBuffer, sizeof(rangeEditBuffer), "%i", editSpeaker->range );
}

void CG_UndoEditSpeaker( void )
{
	if( undoSpeakerIndex == -2 )
		return;

	if( undoSpeakerIndex == -1 ) {
		if( !BG_SS_StoreSpeaker( &undoSpeaker ) )
			CG_Printf( S_COLOR_YELLOW "UNDO: restoring deleted speaker failed, no storage memory for speaker\n" );
		else
			CG_Printf( "UNDO: restored deleted speaker at %.2f %.2f %.2f.\n", undoSpeaker.origin[0], undoSpeaker.origin[1], undoSpeaker.origin[2] );
	} else {
		bg_speaker_t *speaker = BG_GetScriptSpeaker( undoSpeakerIndex );
		memcpy( speaker, &undoSpeaker, sizeof(*speaker) );
		CG_Printf( "UNDO: restoring modified settings of speaker at %.2f %.2f %.2f.\n", undoSpeaker.origin[0], undoSpeaker.origin[1], undoSpeaker.origin[2] );
	}

    CG_SaveSpeakersToScript();
	undoSpeakerIndex = -2;
}

// Normal Use

void CG_ToggleActiveOnScriptSpeaker( int index )
{
	bg_speaker_t *speaker = BG_GetScriptSpeaker( index );

	if( speaker ) {
		speaker->activated = !speaker->activated;
	}
}

void CG_UnsetActiveOnScriptSpeaker( int index )
{
	bg_speaker_t *speaker = BG_GetScriptSpeaker( index );

	if( speaker ) {
		speaker->activated = qfalse;
	}
}

void CG_SetActiveOnScriptSpeaker( int index )
{
	bg_speaker_t *speaker = BG_GetScriptSpeaker( index );

	if( speaker ) {
		speaker->activated = qtrue;
	}
}

static void CG_PlayScriptSpeaker( bg_speaker_t *speaker, qboolean global )
{
	switch( speaker->loop ) {
	case S_LT_NOT_LOOPED:	if( global ) {
								trap_S_StartLocalSound( speaker->noise, CHAN_ITEM );
							} else {
								trap_S_StartSoundVControl( speaker->origin, -1, CHAN_ITEM, speaker->noise, speaker->volume );
							}
							break;
	case S_LT_LOOPED_ON:
	case S_LT_LOOPED_OFF:	if( speaker->soundTime == 0 )
								speaker->soundTime = trap_S_GetCurrentSoundTime();
							trap_S_AddRealLoopingSound( speaker->origin, vec3_origin, speaker->noise, speaker->range, speaker->volume, speaker->soundTime );
							break;
	}
}

void CG_AddScriptSpeakers( void )
{
	int				i;
	bg_speaker_t	*speaker;

	if( cg.editingSpeakers ) {
		CG_RenderScriptSpeakers();
	}

	for( i = 0; i < BG_NumScriptSpeakers(); i++ ) {
		speaker = BG_GetScriptSpeaker( i );

		// don't bother playing missing sounds
		if( !speaker->noise ) {
			continue;
		}

		// activate if needed
		if( speaker->loop == S_LT_NOT_LOOPED ) {
			if( cg.time >= speaker->nextActivateTime && ( speaker->wait || speaker->random ) ) {
				speaker->activated = qtrue;
				speaker->nextActivateTime = cg.time + speaker->wait + speaker->random * crandom();
			}
		}

		if( !speaker->activated ) {
			speaker->soundTime = 0;
			continue;
		}

		switch( speaker->broadcast ) {
		case S_BT_LOCAL:	if( trap_R_inPVS( cg.refdef_current->vieworg, speaker->origin ) ) {
								CG_PlayScriptSpeaker( speaker, qfalse );
							}
							break;
		case S_BT_GLOBAL:	CG_PlayScriptSpeaker( speaker, qtrue );
							break;
		case S_BT_NOPVS:	CG_PlayScriptSpeaker( speaker, qfalse );
							break;
		}

		if( speaker->loop == S_LT_NOT_LOOPED ) {
			speaker->activated = qfalse;
		}
	}
}
